﻿// ==================================================================================================
// Copyright(C) 2012 NCS Pte. Ltd. All rights reserved.
//
// SYSTEM NAME	    : iConnect .Net Application Framework
// COMPONENT ID	    : NCS.IConnect.Core
// COMPONENT DESC   : The foundation component of the whole framework. 	
//
// CREATED DATE/BY  : 13 Apr 2012 / Wu Yong Qing
//
// REVISION HISTORY :	
// DATE/BY			SR#/CS/PM#/OTHERS		DESCRIPTION OF CHANGE
// ==================================================================================================

using NCS.IConnect.Core.Properties;
using NCS.IConnect.Core.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

namespace NCS.IConnect.Core.IoC.TransientCall
{
    /// <summary>
    /// Transient call proxy class generator.
    /// </summary>
    public class TransientCallProxyClassGenerator
    {
        private static readonly AssemblyBuilder assemblyBuilder;
        private readonly Type typeToIntercept;
        private TypeBuilder typeBuilder;
        private FieldBuilder fbInstanceProvider;

        static TransientCallProxyClassGenerator()
        {
            assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("NCS_iConnect_ILEmit_InterfaceInterceptionProxies"), AssemblyBuilderAccess.Run);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="TransientCallProxyClassGenerator"/> class.
        /// </summary>
        /// <param name="typeToIntercept">The type to intercept.</param>
        public TransientCallProxyClassGenerator(Type typeToIntercept)
        {
            Guard.ArgumentNotNull(typeToIntercept, "typeToIntercept");
            if (!typeToIntercept.IsInterface || typeToIntercept.IsGenericType || typeToIntercept.IsGenericTypeDefinition)
            {
                throw new ArgumentException(ResourceUtility.Format(Resources.ExceptionTransientCallInterceptOnlyForNonGenericInterface));
            }

            this.typeToIntercept = typeToIntercept;
            this.CreateTypeBuilder();
        }

        private void CreateTypeBuilder()
        {
            var typeName = "DynamicModule.ns.Wrapped_" + this.typeToIntercept.Name + "_" + Guid.NewGuid().ToString("N");
            var moduleName = Guid.NewGuid().ToString("N");
            var moduleBuilder = assemblyBuilder.DefineDynamicModule(moduleName);
            this.typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public);

            this.ImplementInstanceProviderAware();
        }

        private void ImplementInstanceProviderAware()
        {
            this.typeBuilder.AddInterfaceImplementation(typeof(IInstanceProviderAware));

            this.fbInstanceProvider = this.typeBuilder.DefineField("instanceProvider", typeof(IInstanceProvider), FieldAttributes.Private);
            var pbInstanceProvider = this.typeBuilder.DefineProperty("InstanceProvider", PropertyAttributes.HasDefault, typeof(IInstanceProvider), null);

            var mbInstanceProviderGetAccessor = this.typeBuilder.DefineMethod(
                "get_InstanceProvider",
                MethodAttributes.Public | MethodAttributes.Virtual,
                typeof(IInstanceProvider),
                null);

            var il = mbInstanceProviderGetAccessor.GetILGenerator();
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldfld, this.fbInstanceProvider);
            il.Emit(OpCodes.Ret);

            pbInstanceProvider.SetGetMethod(mbInstanceProviderGetAccessor);
        }

       

        private void CreateConstructor()
        {
            var cbCtor = this.typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new[] { typeof(IInstanceProvider) });
            var il = cbCtor.GetILGenerator();
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes));
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Stfld, this.fbInstanceProvider);
            il.Emit(OpCodes.Ret);
        }

        private void ImplementTargetInterface()
        {
            this.typeBuilder.AddInterfaceImplementation(this.typeToIntercept);
            List<MethodInfo> methods = new List<MethodInfo>();
            this.GetMethods(methods, this.typeToIntercept);
            foreach (var mi in methods)
            {
                var paramTypes = mi.GetParameters().Select(p => p.ParameterType).ToArray();
                var mbMethod = this.typeBuilder.DefineMethod(
                    mi.Name,
                    MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual | MethodAttributes.Final,
                    mi.ReturnType,
                    paramTypes);

                this.CreateTargetInterfaceMethod(mi, mbMethod, paramTypes);
            }
        }

         private void GetMethods(List<MethodInfo> methods, Type interfaceType)
        {
            methods.AddRange(interfaceType.GetMethods(BindingFlags.Public | BindingFlags.Instance));
            foreach (Type parent in interfaceType.GetInterfaces())
            {
                GetMethods(methods, parent);
            }
        }
        private void CreateTargetInterfaceMethod(MethodInfo methodInfo, MethodBuilder mbMethod, Type[] paramTypes)
        {
            var il = mbMethod.GetILGenerator();
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldfld, this.fbInstanceProvider);
            il.Emit(OpCodes.Callvirt, typeof(IInstanceProvider).GetMethod("GetInstance"));
            il.Emit(OpCodes.Castclass, this.typeToIntercept);
            for (int i = 1; i <= paramTypes.Length; i++)
            {
                il.Emit(OpCodes.Ldarg, i);
            }
            il.Emit(OpCodes.Callvirt, methodInfo);
            il.Emit(OpCodes.Ret);
        }

        /// <summary>
        /// Creates the type of the interception proxy.
        /// </summary>
        /// <returns></returns>
        public Type CreateProxyType()
        {
            this.CreateConstructor();
            this.ImplementTargetInterface();

            return this.typeBuilder.CreateType();
        }
    }
}